﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Scheme.Net.Runtime.AST;

namespace Scheme.Net.Runtime.Evaluator
{
    internal static class Rewriters
    {
        #region Fields

        private static int rewriteCounter;

        #endregion

        #region Construction

        static Rewriters() { rewriteCounter = 0; }

        #endregion

        #region Delayed Evaluation

        public static IEvaluable MakePromise(Pair exp)
        {
            var promiseProc = Runtime.Parser.SchemeParser.Parse(
                string.Format(@"((lambda (proc)
                                    (let ((result-ready? #f)
                                          (result #f))
                                      (lambda ()
                                        (if result-ready?
                                            result
                                            (let ((x (proc)))
                                              (if result-ready?
                                                  result
                                                  (begin (set! result-ready? #t)
                                                         (set! result x)
                                                         result))))))) (lambda () {0}))",
                              ((Pair)exp).Cadr));

            return promiseProc;
        }

        #endregion

        #region Quasiquoation

        public static IEvaluable ExpandQuasiquote(Pair exp)
        {
            Func<IEvaluable, int, IEvaluable> QQExpand = null;
            Func<IEvaluable, int, IEvaluable> QQExpandList = null;

            QQExpand = (x, depth) =>
            {
                if (x.IsPair())
                {
                    var p = (Pair)x;
                    if (p.Car == Symbol.Lookup("QUASIQUOTE"))
                        return Pair.CreateList(new IEvaluable[]
                        {
                            Symbol.Lookup("CONS"),
                            Pair.CreateList(new IEvaluable[] { Symbol.Lookup("QUOTE"), Symbol.Lookup("QUASIQUOTE") }),
                            QQExpand(p.Cdr, depth + 1)
                        });
                    else if (p.Car == Symbol.Lookup("UNQUOTE") || p.Car == Symbol.Lookup("UNQUOTE-SPLICING"))
                    {
                        if (depth > 0)
                            return Pair.CreateList(new IEvaluable[]
                            {
                                Symbol.Lookup("CONS"),
                                Pair.CreateList(new IEvaluable[] { Symbol.Lookup("QUOTE"), p.Car }),
                                QQExpand(p.Cdr, depth - 1)
                            });
                        else if (p.Car == Symbol.Lookup("UNQUOTE") && !p.Cdr.IsNull() && p.Cddr.IsNull())
                            return p.Cadr;
                        else
                            throw new EvaluatorException(string.Format("Illegal quasiquoted form: {0}", x));
                    }
                    else
                        return Pair.CreateList(new IEvaluable[]
                        {
                            Symbol.Lookup("APPEND"),
                            QQExpandList(p.Car, depth),
                            QQExpand(p.Cdr, depth)
                        });
                }
                else
                    return Pair.CreateList(new IEvaluable[] { Symbol.Lookup("QUOTE"), x });
            };

            QQExpandList = (x, depth) =>
            {
                if (x.IsPair())
                {
                    var p = (Pair)x;
                    if (p.Car == Symbol.Lookup("QUASIQUOTE"))
                        return Pair.CreateList(new IEvaluable[]
                        {
                            Symbol.Lookup("LIST"),
                            Pair.CreateList(new IEvaluable[]
                            {
                                Symbol.Lookup("CONS"),
                                Pair.CreateList(new IEvaluable[] { Symbol.Lookup("QUOTE"), Symbol.Lookup("QUASIQUOTE")} ),
                                QQExpand(p.Cdr, depth + 1)
                            })
                        });
                    else if (p.Car == Symbol.Lookup("UNQUOTE") || p.Car == Symbol.Lookup("UNQUOTE-SPLICING"))
                    {
                        if (depth > 0)
                            return Pair.CreateList(new IEvaluable[]
                            {
                                Symbol.Lookup("LIST"),
                                Pair.CreateList(new IEvaluable[]
                                {
                                    Symbol.Lookup("CONS"),
                                    Pair.CreateList(new IEvaluable[] { Symbol.Lookup("QUOTE"), p.Car }),
                                    QQExpand(p.Cdr, depth - 1)
                                })
                            });
                        else if (p.Car == Symbol.Lookup("UNQUOTE"))
                            return new Pair(Symbol.Lookup("LIST"), p.Cdr);
                        else
                            return new Pair(Symbol.Lookup("APPEND"), p.Cdr);
                    }
                    else
                        return Pair.CreateList(new IEvaluable[]
                        {
                            Symbol.Lookup("LIST"),
                            Pair.CreateList(new IEvaluable[]
                            {
                                Symbol.Lookup("APPEND"),
                                QQExpandList(p.Car, depth),
                                QQExpand(p.Cdr, depth)
                            })
                        });
                }
                else
                    return Pair.CreateList(new IEvaluable[]
                    {
                        Symbol.Lookup("QUOTE"),
                        Pair.CreateList(new IEvaluable[] { x })
                    });
            };

            return QQExpand(((Pair)exp).Cadr, 0);
        }

        #endregion

        #region Cond

        public static IEvaluable CondToIf(Pair exp)
        {
            Func<Pair, IEvaluable> SequenceToExp = seq =>
            {
                if (seq.IsNull()) return seq;
                else if (seq.Cdr == Pair.Empty) return seq.Car;
                else return new Pair(Symbol.Lookup("BEGIN"), seq);
            };

            Func<IEvaluable, Pair> CondClauses = e =>
            {
                return (Pair)((Pair)e).Cdr;
            };

            Func<Pair, IEvaluable> CondPredicate = clause => 
            {
                return clause.Car;
            };

            Func<Pair, IEvaluable> CondActions = clause =>
            {
                return clause.Cdr;
            };

            Func<Pair, IEvaluable> CondProc = clause =>
            {
                return clause.Caddr;
            };

            Func<Pair, bool> IsCondElseClause = clause =>
            {
                return CondPredicate(clause) == Symbol.Lookup("ELSE");
            };

            Func<Pair, IEvaluable> ExpandClauses = null;
            ExpandClauses = clauses =>
            {
                Func<Pair, bool> IsArrow = c => c.Cadr == Symbol.Lookup("=>");
                Func<IEvaluable, IEvaluable, IEvaluable, Pair> MakeIf = (pred, cons, alt) =>
                    Pair.CreateList(new IEvaluable[]
                {
                    Symbol.Lookup("IF"),
                    pred,
                    cons,
                    alt
                });
                Func<IEvaluable, IEvaluable, Pair> MakeLet = (binds, body) => new Pair(Symbol.Lookup("LET"), new Pair(binds, body));

                if (clauses.IsNull())
                    return Symbol.Unspecified;
                else
                {
                    var first = clauses.Car as Pair;
                    var rest = clauses.Cdr as Pair;

                    if (!IsCondElseClause(first))
                    {
                        if (!IsArrow(first))
                            return MakeIf(CondPredicate(first), SequenceToExp((Pair)CondActions(first)), ExpandClauses(rest));
                        if (!first.Cdddr.IsNull())
                            throw new EvaluatorException("Bad use of => - ExpandClauses");
                        var g = Symbol.GenerateSymbol();
                        return MakeLet(Pair.CreateList(new IEvaluable[] { Pair.CreateList(new IEvaluable[] { g, CondPredicate(first) }) }),
                            new Pair(MakeIf(g, new Pair(first.Caddr, new Pair(g, Pair.Empty)), ExpandClauses(rest)), Pair.Empty));
                    }
                    else if (!rest.IsNull())
                        throw new EvaluatorException("ELSE clause isn't last - ExpandClauses");
                    else if (IsArrow(first))
                        throw new EvaluatorException("Bad clause form - ExpandClauses");
                    else
                        return SequenceToExp((Pair)CondActions(first));
                }
            };

            return ExpandClauses(CondClauses(exp));
        }

        #endregion

        #region Case

        public static IEvaluable CaseToIf(Pair exp)
        {
            var key = exp.Cadr;
            var clauses = ((Pair)exp.Cddr).Values.Select(c =>
            {
                var first = ((Pair)c).Car;
                if (first == Symbol.Lookup("ELSE"))
                    return c;
                else
                    return new Pair(
                        Pair.CreateList(new IEvaluable[]
                        {
                            Symbol.Lookup("MEMV"),
                            key,
                            new Pair(Symbol.Lookup("QUOTE"), new Pair(first, Pair.Empty))
                        }),
                        ((Pair)c).Cdr);
            });
            var result = new Pair(Symbol.Lookup("COND"), Pair.CreateList(clauses));
            return CondToIf(result);
        }

        #endregion

        #region Let and Friends

        #region Let

        public static IEvaluable LetToCombination(Pair exp)
        {
            var e = exp as Pair;
            if (e.Cadr.IsPair() || e.Cadr.IsNull())
            {
                var variables = Pair.CreateList(((Pair)e.Cadr).Values.Select(v => ((Pair)v).Car));
                var values = Pair.CreateList(((Pair)e.Cadr).Values.Select(v => ((Pair)v).Cadr));
                var body = e.Cddr;
                var result = new Pair(
                    new Pair(Symbol.Lookup("LAMBDA"), new Pair(variables, body)),
                    values);
                return result;
            }
            else
            {
                var innerName = (Symbol)e.Cadr;
                var variables = Pair.CreateList(((Pair)e.Caddr).Values.Select(v => ((Pair)v).Car));
                var values = Pair.CreateList(((Pair)e.Caddr).Values.Select(v => ((Pair)v).Cadr));
                var body = e.Cdddr;
                var result = Pair.CreateList(new IEvaluable[] 
                    {
                        Symbol.Lookup("LET"),
                        Pair.Empty,
                        new Pair(Symbol.Lookup("DEFINE"),
                            new Pair(new Pair(innerName, variables), 
                            body)),
                        new Pair(innerName, values)
                    });
                return result;
            }
        }

        #endregion

        #region Let*

        public static IEvaluable LetStarToNestedLets(Pair exp)
        {

            Func<Pair, IEvaluable, Pair> MakeCascadingLets = null;
            MakeCascadingLets = (parms, b) =>
            {
                if (parms.Cdr.IsNull())
                    return new Pair(Symbol.Lookup("LET"), new Pair(Pair.CreateList(new IEvaluable[] { parms.Car }), b));
                else
                    return Pair.CreateList(new IEvaluable[] {
                            Symbol.Lookup("LET"),
                            Pair.CreateList(new IEvaluable[] { parms.Car }),
                            MakeCascadingLets((Pair)parms.Cdr, b)
                        });
            };

            var body = exp.Cddr;

            var result = MakeCascadingLets((Pair)exp.Cadr, body);
            return result;
        }

        #endregion

        #region Letrec

        public static IEvaluable LetRecToLet(Pair exp)
        {
            var bindings = ((Pair)exp.Cadr).Values;
            var body = ((Pair)exp.Cddr).Values;
            var sb = new StringBuilder();
            sb.Append("(LET (");
            foreach (var b in bindings)
                sb.Append(string.Format("({0} (UNDEFINED-VALUE))", ((Pair)b).Car));
            sb.Append(")");
            foreach (var b in bindings)
                sb.Append(string.Format("(SET! {0} {1})", ((Pair)b).Car, ((Pair)b).Cadr));
            foreach (var b in body)
                sb.Append(b);
            sb.Append(")");

            var result = Runtime.Parser.SchemeParser.Parse(sb.ToString());
            return result;
        }

        #endregion

        #endregion

        #region Or

        public static IEvaluable OrToIf(Pair exp)
        {
            var terms = ((Pair)exp.Cdr).Values;

            if (terms.Count() == 0)
                return Symbol.False;

            var rewrittenTerms = terms.Select(t =>
            {
                var g = GenerateIdentifier();
                return string.Format("(LET (({0} {1})) (IF {0} {0}", g.Name, t);
            }).Aggregate((a, s) => a + s) + " #F";

            foreach (var t in terms)
                rewrittenTerms += "))";

            return Runtime.Parser.SchemeParser.Parse(rewrittenTerms);
        }

        #endregion

        #region And

        public static IEvaluable AndToIf(Pair exp)
        {
            var terms = ((Pair)exp.Cdr).Values;

            if (terms.Count() == 0)
                return Symbol.True;
            else if (terms.Count() == 1)
                return terms.First();
            else
            {
                var e1 = terms.First();
                var es = Pair.CreateList(terms.Skip(1));

                return Pair.CreateList(new IEvaluable[]
                {
                    Symbol.Lookup("IF"),
                    e1,
                    AndToIf(new Pair(Symbol.Lookup("_"), es)),
                    Symbol.False
                });
            }
        }

        #endregion

        #region Multiple Values

        public static IEvaluable ReceiveToCWV(Pair e)
        {
            var exp = (Pair)e;
            var formals = exp.Cadr;
            var expr = exp.Caddr;
            var body = exp.Cddr;

            return Pair.CreateList(new IEvaluable[]
            {
                Symbol.Lookup("CALL-WITH-VALUES"),
                Pair.CreateList(new IEvaluable[]
                {
                    Symbol.Lookup("LAMBDA"),
                    Pair.Empty,
                    expr
                }),
                Pair.CreateList(new IEvaluable[]
                {
                    Symbol.Lookup("LAMBDA"),
                    formals
                }.Concat(((Pair)body).Values))
            });
        }

        #endregion

        #region Utilities

        public static Symbol GenerateIdentifier()
        {
            return Symbol.Lookup("GEN" + rewriteCounter++);
        }

        public static Pair ScanOutDefines(Pair body)
        {
            var pureBody = body.Values.Where(v => !v.IsTaggedList("DEFINE"));
            var defines = body.Values.Where(v => v.IsTaggedList("DEFINE"));
            if (defines.Count() == 0) return body;

            Func<Pair, IEvaluable> DefinitionVariable = e => e.Cadr is Symbol ? (Symbol)e.Cadr : (Symbol)e.Caadr;
            Func<Pair, IEvaluable> DefinitionValue = e => e.Cadr is Symbol ? e.Caddr : new Pair(Symbol.Lookup("LAMBDA"), new Pair(e.Cdadr, e.Cddr));

            var result = Pair.CreateList(new IEvaluable[] {
                Pair.CreateList(new IEvaluable[]
                {
                    Symbol.Lookup("LET"),
                    Pair.CreateList(
                        defines.Select(d => 
                            new Pair(DefinitionVariable((Pair)d), 
                                new Pair(
                                    new Pair(Symbol.Lookup("UNDEFINED-VALUE"), Pair.Empty), 
                                    Pair.Empty))))
                }
                .Concat(
                    defines.Select(d => Pair.CreateList(new IEvaluable[] 
                    {
                        Symbol.Lookup("SET!"), 
                        DefinitionVariable((Pair)d),
                        DefinitionValue((Pair)d)
                    })))
                .Concat(pureBody)
                )});
            return result;
        }

        #endregion
    }
}
